import mido

# mido里面note编号和音阶对应
# {编号}-{音阶}拼接成blender模型中的对应琴键的形态键名，如“9-A0”，“48-C4”
note_dict={
    9: 'A0',
    10: 'Bb0',
    11: 'B0',
    12: 'C1',
    13: 'Db1',
    14: 'D1',
    15: 'Eb1',
    16: 'E1',
    17: 'F1',
    18: 'F#1',
    19: 'G1',
    20: 'G#1',
    21: 'A1',
    22: 'Bb1',
    23: 'B1',
    24: 'C2',
    25: 'Db2',
    26: 'D2',
    27: 'Eb2',
    28: 'E2',
    29: 'F2',
    30: 'F#2',
    31: 'G2',
    32: 'G#2',
    33: 'A2',
    34: 'Bb2',
    35: 'B2',
    36: 'C3',
    37: 'Db3',
    38: 'D3',
    39: 'Eb3',
    40: 'E3',
    41: 'F3',
    42: 'F#3',
    43: 'G3',
    44: 'G#3',
    45: 'A3',
    46: 'Bb3',
    47: 'B3',
    48: 'C4',
    49: 'Db4',
    50: 'D4',
    51: 'Eb4',
    52: 'E4',
    53: 'F4',
    54: 'F#4',
    55: 'G4',
    56: 'G#4',
    57: 'A4',
    58: 'Bb4',
    59: 'B4',
    60: 'C5',
    61: 'Db5',
    62: 'D5',
    63: 'Eb5',
    64: 'E5',
    65: 'F5',
    66: 'F#5',
    67: 'G5',
    68: 'G#5',
    69: 'A5',
    70: 'Bb5',
    71: 'B5',
    72: 'C6',
    73: 'Db6',
    74: 'D6',
    75: 'Eb6',
    76: 'E6',
    77: 'F6',
    78: 'F#6',
    79: 'G6',
    80: 'G#6',
    81: 'A6',
    82: 'Bb6',
    83: 'B6',
    84: 'C7',
    85: 'Db7',
    86: 'D7',
    87: 'Eb7',
    88: 'E7',
    89: 'F7',
    90: 'F#7',
    91: 'G7',
    92: 'G#7',
    93: 'A7',
    94: 'Bb7',
    95: 'B7',
    96: 'C8',
}

# 将要读取的midi文件路径，必须绝对路径，注意双写\\
mid_file='C:\\Users\\Leo\\Desktop\\midi\\deepsea2.mid'
mid = mido.MidiFile(mid_file)
# midi文件内部参数，一拍的tick数量，后面算真实时长要用到
mid_ticks_per_beat=mid.ticks_per_beat
print(f'mid_ticks_per_beat={mid_ticks_per_beat}')

# 读取midi文件BPM数值，后面可以算出一拍的真实时长
# midi文件允许BPM中途改变，但我做不到所以不运行，有多个BPM值时直接退出
mid_bpm=None
mid_bpm_count=0
for i, track in enumerate(mid.tracks):
    for msg in track:
        if msg.type=='set_tempo':
            mid_bpm_count+=1
            mid_bpm=60000000/msg.tempo
            print(f'mid_bpm_count={mid_bpm_count}, mid_bpm={mid_bpm}')
if mid_bpm_count!=1:
    print('Error! BPM number not constant! Unimplemented!')
    exit()

# 根据BPM计算一拍真实时长
mid_sec_per_beat=60/mid_bpm
print(f'mid_sec_per_beat={mid_sec_per_beat}')

# midi里音符的长度单位是tick
# 计算真实时间的1秒有多少tick，后面换算用
mid_ticks_per_sec=mid_ticks_per_beat/mid_sec_per_beat
print(f'mid_ticks_per_sec={mid_ticks_per_sec}')
# mid_sec_per_tick=mid_sec_per_beat/mid_ticks_per_beat
# print(f'mid_sec_per_tick={mid_sec_per_tick}')

# 以下描述并不严谨，仅作意会！
# midi记录的是事件event，比如设定bpm事件，更换乐器事件，音符“按下”事件，音符“抬起”事件等
# 所以一次按键由两个一对事件组成，对应“按下”和“抬起”
# 记录下读取的事件，发现抬起事件时，在记录中从后往前找相同音阶的最近一个事件，不加验证地认为这就是对应的按下事件
notes=[]
def find_last_note(name: str):
    size=len(notes)
    for i in range(size):
        note=notes[size-1-i]
        if note['name']==name:
            return note
    return None
for i, track in enumerate(mid.tracks):
    total_time=0
    for msg in track:
        if msg.type=='note_on':
            # print(msg)
            note={}
            # midi共有128个音阶，但88键钢琴只有88个，做了限定
            try:
                note['name']=note_dict[msg.note]
            except:
                print('Error! Note key out of range! A0~C8 supported! (88 keys piano)')
                exit()
            note['tag']=f'{msg.note:0>2}-{note_dict[msg.note]}'
            # 不加验证地认为velocity值为0时就是抬起，反之就是按下
            note_type='begin' if msg.velocity>0 else 'end'
            # midi中的tick值记录的时间不是从头开始的，是从上一个事件开始，所以要累计
            note_length=msg.time/mid_ticks_per_sec
            total_time+=note_length
            note['time']=total_time
            # 如果是按下事件则记录，如果是抬起事件则往回找对应按下事件，补全音符时长
            if note_type=='begin':
                notes.append(note)
            else:
                begin_note=find_last_note(note['name'])
                begin_note['length']=total_time-begin_note['time']
                begin_note['long']=note_length>mid_sec_per_beat
            # print(note)

# print('result')
# for note in notes:
#     print(note)

# 生成动画的开始帧，即动画从这一帧开始，即midi文件播放从这一帧开始，不是第一个音符从这一帧开始
FRAME_START_OFFSET=30
# 生成动画时，如果音符长度不足一拍，则按下后立即抬起
# 如果音符时长足够一拍，则按下后，经过音符时长后、提前若干帧抬起
# 为了造成一种琴键松开后声音经过短暂延迟后再消失的效果
# 初步设定琴键抬起比声音消失提前20帧
FRAME_LONG_OFFSET=-20

# 提前3帧琴键开始往下按
FRAME_BEGIN=-3
# 琴键被按到最下时声音发出
FRAME_PEAK=0
# 经过3帧后琴键完全回弹
FRAME_END=3

import bpy

# blender场景的帧率设置，影响真实时间和关键帧帧数的计算
frame_rate=30
# 读取当前活动物体（琴键网格）的形态键设置
shape_keys=bpy.context.active_object.data.shape_keys
key_blocks=shape_keys.key_blocks
#for key_block in key_blocks:
#    key_block.value=0
#    key_block.keyframe_insert('value', frame=1)
for note in notes:
    # 短音符：用3帧按下，声音发出，再用3帧回弹
    frame=note['time']*frame_rate+FRAME_START_OFFSET
    shape_keys.key_blocks[note['tag']].value=0
    shape_keys.keyframe_insert('key_blocks["'+note['tag']+'"].value', frame=frame+FRAME_BEGIN)
    shape_keys.key_blocks[note['tag']].value=1
    shape_keys.keyframe_insert('key_blocks["'+note['tag']+'"].value', frame=frame+FRAME_PEAK)
    if note['long']:
        # 长音符：用3帧按下，声音发出，声音停止前20帧开始回弹，用3帧回弹到原位，回弹到原位后经过20-3=17帧后声音停止
        frame+=note['length']*frame_rate+FRAME_LONG_OFFSET
        shape_keys.key_blocks[note['tag']].value=1
        shape_keys.keyframe_insert('key_blocks["'+note['tag']+'"].value', frame=frame+FRAME_PEAK)
    shape_keys.key_blocks[note['tag']].value=0
    shape_keys.keyframe_insert('key_blocks["'+note['tag']+'"].value', frame=frame+FRAME_END)